package qmlt.learning.neuralnetwork.node;

import java.util.ArrayList;
import java.util.List;

public class Node
{
    public String      id;

    public List<Float> inputs   = new ArrayList<Float>();

    public List<Float> weights  = new ArrayList<Float>();

    protected float    unthreshOutput;

    protected float    output;

    public float       errorTerm;

    public List<Edge>  inlinks  = new ArrayList<Edge>();

    public List<Edge>  outlinks = new ArrayList<Edge>();

    public Node(String id)
    {
        this.id = id;
        addInput();
    }

    public void addInput()
    {
        // init weight in [-0.05, 0.05)
        float initWeight = (float) (Math.random() * 0.1 - 0.05);
        addInput(initWeight);
    }

    public void addInput(float initWeight)
    {
        inputs.add(new Float(1));
        weights.add(initWeight);
    }

    public void connectTo(Node node)
    {
        int n = node.inputs.size();
        node.addInput();
        connectTo(node, n);
    }

    public void connectTo(Node node, int toIndex)
    {
        Edge e = new Edge(this, node, toIndex);
        this.outlinks.add(e);
        node.inlinks.add(e);
    }

    public void feedForward()
    {
        assert (inputs.size() == weights.size());

        float rst = 0;
        int size = inputs.size();
        for (int i = 0; i < size; ++i)
        {
            rst += inputs.get(i) * weights.get(i);
        }

        unthreshOutput = rst;
        output = squash(rst);

        for (Edge e : outlinks)
        {
            e.to.inputs.set(e.toIndex, output);
        }
    }

    public void backPropagate()
    {
        assert (inputs.size() == weights.size());

        float rst = 0;
        for (Edge e : outlinks)
        {
            rst += e.to.weights.get(e.toIndex) * e.to.errorTerm;
        }
        errorTerm = output * (1 - output) * rst;
    }

    public List<Float> previousDeltas;

    public void updateWeights(double eta, double momentumCoef, double decay)
    {
        assert (inputs.size() == weights.size());

        if (previousDeltas == null)
        {
            previousDeltas = new ArrayList<Float>();
            for (int i = 0; i < weights.size(); ++i)
            {
                previousDeltas.add((float) 0);
            }
        }

        List<Float> deltas = new ArrayList<Float>();
        for (int i = 0; i < weights.size(); ++i)
        {
            float delta = (float) (eta * errorTerm * inputs.get(i) + momentumCoef
                    * previousDeltas.get(i));
            deltas.add(delta);
            weights.set(i, (float) (1.0 - decay) * weights.get(i) + delta);
        }
        previousDeltas = deltas;
    }

    public float squash(float x)
    {
        return (float) (1.0 / (1.0 + Math.exp(-x)));
    }
}
